World of Warcraft Token Trader Algorithm Results

The video game World of Warcraft allows players to purchase a WoW Token for 15 dollars. Within the game, these tokens can be sold in an Auction House for in-game currency (Gold). This provides an exchange rate for the region's currency and World of Warcraft currency.

This trading algorithm operated using an ARIMA(5,1,5) model that was trained on US-user data spanning early 2015 to present. The backtest was performed on EU data spanning that same range.

The starting conditions were 1000 Euros and 200,00 Gold, which converts roughly to 1107 Euros after combining. The end value after trading for the 5 year span came out to roughly 47.3k Euros.


The Algorithm: Logic, Mathematics

Pricing Functions

In order to better model currency markets, bid and ask prices were computed as a function of the volume. Buying or selling 1 token will approximate the market price. However, in order to buy or sell a certain quantity, the price must be adjusted to meet the quantity requested/offered.

In supply-demand plots, the price can define the quantity or vice versa. In this scenario we want to solve for the price.

Supply-Demand

For example, assume an equilibrium price of 100 Euros and equilibrium quantity of 7 Tokens. If we want to sell 20 Tokens, our ask price will have to decrease in order to find buyers. One the other hand, when trying to buy 20 Tokens, our bid price will have to increase in order to find sellers.

However, currency markets are different from a service or good market. An increase price (exchange rate) is observed as bad, and money should be moved out of that currency to prevent its depreciation.

Example I have 1 USD. Right now, the exchange rate is 100 Gold per USD. A little bird tells me that the rate will decrease to 50 Gold per USD. If I convert my dollar to gold now, wait for the drop, then convert back to USD, I will have 2 USD.

Therefore, if we anticipate a increase in Gold per USD, we are willing to spend more Gold for each USD. The counter of this is when we predict that the Gold per USD will decrease. In this scenario we want to convert USD to Gold. Here we ought to willing to receive less Gold per USD in order to profit from the deflation.

The backtest encapsulates the pricing relationship with compound functions. I say price, but it is really a rate. This another way of me saying I'm willing to trade at this rate.

Buy Gold Price

$b(v)=m(1-r)^v$

Sell Gold Price

$s(v)=m(1+r)^v$

  • $r$: compounding rate
  • $m$: market exchange rate
  • $v$: volume

Profit

$\pi = v \times (b(v) - s(v))$

$\pi = v \times (m(1-r)^v - \hat{m}(1+r)^v)$

  • $\pi$: profit
  • $\hat{m}$: predicted market exchange rate at sale

With this new profit function we can optimize the volume we ought to buy.

Profit First Derivative

  • $b'(v) = b(v)\ln(1-r)$

  • $s'(v) = s(v)\ln(1+r)$

$\frac{\pi}{dv} = (b(v) + vb'(v)) - (s(v) + vs'(v))$

$\pi' = (b(v) + vb(v)\ln(1-r)) - (s(v) + vs(v)\ln(1+r))$

$\pi' = b(v)(1 + v\ln(1-r)) - s(v)(1 + v\ln(1+r))$

I used a modified bisection algorithm so solve for volumes where the profit derivative equaled 0. Because of the compounding formulas there are no local minimums, only one global maximum. The bisection algorithm was contained to a volume range from 0 to the maximum $vb(v)$ the bank could afford.


Trading Strategy

This algorithm's goal was to move as much volume within the shortest amount of time possible. The available data progresses in 20 minute increments. Because this is a short-term volume trader, the ARIMA model only had to predict one moment ahead, maintaining high confidence levels. An anticipated decrease in the exchange rate resulted in a purchase. A purchase was always followed by a sale, even if the prediction was off the trade lost money. This prevented the accumulation of Gold and possible devaluation of the account due to a trend.

Because of the purchase and sale structure (Can only buy or sell Tokens, which are 15 USD regardless of the gold value), profits went into the WoW account. In order to prevent a devaluation trend from offsetting profits, profits were funneled into the regional currency account when there was enough to sell 1 Token at market price. This assumes the regional currency is more stable than WoW Gold, yet this will do for the backtest.


Starting Parameters


Model Training Data Region: US

Training Data Date Range: April 12, 2015 @ 5:36 PM to June 11, 2020 @ 7:15 PM

Observations: 120,043


Region: EU

Regional Token Price: 20


Starting Regional Value: 1000

Starting WoW Gold Value: 200,000

Trade Price Compound Rate: 0.00001


Start Date: April 21, 2015 @ 5:35 PM

Regional Start Price: 37,154

In [1]:
import pandas as pd
import numpy as np
import plotly.express as px
from scipy.stats import linregress, chi2_contingency
In [2]:
token = 20
df = pd.read_csv("../data/testrun.txt")
df['net'] = df['regionbank'] + df['wowbank'] * token / df['market_price']
df['error'] = df['market_price'].shift(periods=-1) - df['prediction']
df['profit'] = df['net'].diff(periods=1).shift(periods=-1)
df['predicted'] = (df['prediction'] - df['market_price']).shift(periods=1)
df
Out[2]:
time market_price prior_price prediction trade tradeprice volume regionbank wowbank taper runtime net error profit predicted
0 2015-04-21 17:35:11 37154.0 36786.0 35538.735758 buy 37135.798909 49 20 2.019654e+06 0 0.000124 1107.179925 1987.264242 -11.257668 NaN
1 2015-04-21 17:49:50 37526.0 37154.0 36521.726303 sell 37544.392154 49 1060 6.740093e+04 0 0.000060 1095.922257 1379.273697 -0.896084 -1615.264242
2 2015-04-21 18:06:11 37901.0 37526.0 37240.587828 buy 37881.296505 52 20 2.037228e+06 0 0.000104 1095.026173 1039.412172 -11.184483 -1004.273697
3 2015-04-21 18:20:22 38280.0 37901.0 36196.402752 sell 38299.910677 52 1060 4.563299e+04 0 0.000050 1083.841690 2466.597248 -0.776841 -660.412172
4 2015-04-21 18:34:37 38663.0 38280.0 37123.553352 buy 38642.900366 52 20 2.055064e+06 0 0.000100 1083.064849 1926.446648 -11.076305 -2083.597248
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
118995 2020-06-05 23:10:40 166592.0 166547.0 166727.679520 hold 166592.000000 0 47300 2.041015e+05 0 0.000038 47324.503152 -35.679520 -0.014700 205.633854
118996 2020-06-05 23:30:40 166692.0 166592.0 166850.433471 hold 166692.000000 0 47300 2.041015e+05 0 0.000038 47324.488452 -25.433471 -0.019523 135.679520
118997 2020-06-05 23:50:40 166825.0 166692.0 167012.425795 hold 166825.000000 0 47300 2.041015e+05 0 0.000038 47324.468929 338.574205 -0.076908 158.433471
118998 2020-06-06 00:10:40 167351.0 166825.0 167755.926464 hold 167351.000000 0 47300 2.041015e+05 0 0.000037 47324.392021 85.073536 -0.071211 187.425795
118999 2020-06-06 00:30:40 167841.0 167351.0 168275.690563 hold 167841.000000 0 47300 2.041015e+05 0 0.000038 47324.320810 NaN NaN 404.926464

119000 rows × 15 columns

In [3]:
px.line(df, x="time", y="net", title="Combined Account Value (Euros)")
In [4]:
start_value = df['regionbank'].iloc[0] + token*df['wowbank'].iloc[0] / df['market_price'].iloc[0]
hold_value = 1000 + token*200000 / df['market_price'].iloc[-1]

print(start_value)
print(hold_value)
1107.1799249193496
1023.832079170167

Algorithm Financial Outcome

The algorithm started with a value of 1107 Euros tranched between the regional bank and the WoW bank. The final outcome was 47,324 Euros after 5 years of trading. That is a 4200% increase over 5 years.


$A=P(1+\frac{r}{n})^{nt}$

Interpretations

That level of growth is tantamount to compounding interest rates of

  • 112% annual $\frac{r}{n}$
  • 46% semiannual $\frac{r}{n}$
  • 6% monthly $\frac{r}{n}$
  • 0.2% daily $\frac{r}{n}$

The algorithm was highly successful and it recovered from high-inflation periods quickly.


Null Hypothesis

Had the algorithm not been run and the starting condition been maintained, the net value of the accounts would have depreciated to 1024 Euros.

In [5]:
px.line(df, x="time", y="market_price", title="Token Market Price (Gold)")
In [6]:
px.histogram(df, x="runtime",
             range_x=[0,0.0004],
             nbins=1000,
             color="trade",
             title="Runtime Distribution")
In [7]:
px.scatter(df, x="time", y="taper", title="Insufficient Funds for Gold Sale")
In [9]:
px.scatter(df[df["trade"]=="buy"], x="error", y="profit", trendline="ols", title="Error and Profit for Buy Moments")

Model Performance

Experiment 1

The ARIMA model returns a price difference since it is on the order of (5,1,5). Had we used a pure mean reversion strategy, we would predict that there is 0 correlation between price differences and that the probability it is positive or negative is 50/50.

Hypotheses

$H_1$: ARIMA predictions will be positively related to the true price difference at an alpha of 0.01.
$H_0$: There is no relationship between the predictions and the true price difference.

Method

OLS regression between moment t's predicted price difference and moment t's true price difference.


Experiment 2

The fundamental purpose of the prediction model is to evaluate whether to buy or sell and to decide how much. The first part depends on the sign (±) accuracy, while the second part depends on the point accuracy. Experiment 1 tested point accuracy so this experiment will test sign accuracy.

Hypotheses

$H_1$: ARIMA predictions will perform better than 50/50 on sign hits and misses at an alpha of 0.01.
$H_0$: ARIMA will do no better than random guessing (50/50).

Method

Chi-Square test between a half-and-half sample and the ARIMA prediction hit/miss count.

Experiment 1

In [10]:
res = linregress(x=df['market_price'].diff(periods=1).loc[1:],
                 y=df['predicted'].loc[1:])
print(f"""\
Slope:          {res[0]}
Intercept:      {res[1]}
Pearson r:      {res[2]}
R-Squared:      {res[2]**2}
P-value:        {res[3]}
Standard Error: {res[4]}
""")

px.scatter(x=df['market_price'].diff(periods=1).loc[1:],
           y=df['predicted'].loc[1:],
           trendline="ols",
           title="Predicted Price Difference against True Price Difference")
Slope:          0.494750346716341
Intercept:      -3.892400633755829
Pearson r:      0.5892235966540328
R-Squared:      0.3471844468539143
P-value:        0.0
Standard Error: 0.001966677805075973

Experiment 2

In [11]:
counts = {'hit': 0,
          'miss': 0}

L = len(df)

counts['hit'] = len([1 for n in (df['market_price'] - df['prior_price'])*df['predicted'] if n > 0])
counts['miss'] = L - counts['hit']


array = [[counts['hit'], counts['miss']], [L/2]*2]

res = chi2_contingency(array)

print(f"""\
Hit: {counts['hit']}
Miss: {counts['miss']}

Random Hit: {L/2}
Random Miss: {L/2}
      
Chi-Square Stat:     {res[0]}
p-value:             {res[1]}
Degrees of Freedom:  {res[2]}
Contingency Table:
{res[3][0]}
{res[3][1]}""")
Hit: 95196
Miss: 23804

Random Hit: 59500.0
Random Miss: 59500.0
      
Chi-Square Stat:     23531.34710435708
p-value:             0.0
Degrees of Freedom:  1
Contingency Table:
[77348. 41652.]
[77348. 41652.]

Experiment Results

Foreword


It is important to note that I did not compare the ARIMA performance against mean reversion. Mean reversion would predict that a positive price difference at time t-1 gives time t a greater-than-50/50 chance of being negative. However, during a few tests not featured here, the exact opposite was true. Positive price differences had a notably higher probability of being positive (the converse for negative was true as well). This implies an unusual trend and autocorrelation in the WoW Token prices that makes it difficult to generalize this ARIMA trading strategy. That is why I decided to compare the ARIMA results against null hypotheses informed by randomness instead of the more appropriate null, mean reversion.

Experiment 1

The alternative hypothesis was confirmed. The ARIMA predictions were positively related to the true price difference.

Experiment 2

The alternative hypothesis was confirmed. The ARIMA model chose positive and negative values better than random.